local TARGET_FPS = 27
local TIME_BETWEEN_UPDATES = .1
local MIN_MOD_VALUE = -0.5
local MAX_MOD_VALUE = 0.5
local MAX_MOD_DECREASE = MIN_MOD_VALUE / 5
local MAX_MOD_INCREASE = MAX_MOD_VALUE / 5

local dataobj = LibStub('LibDataBroker-1.1'):NewDataObject('DynPerf', {type='data source',value='-',suffix='%'})

local options = {
	farclip = { min = 220, max = 777, inst = 450 },
	horizonfarclip = { min = 1305, max = 2112, inst = 1600, },
	environmentDetail = { min = 0.5, max = 1.5, },
	--groundEffectDensity = { min = 16, max = 64, }, -- Gives ugly results
	groundEffectDist = { min = 70, max = 140, },
	weatherDensity  = { min = 0, max = 3, },
}
if tonumber(select(4, GetBuildInfo())) > 30000 then
	options.particleDensity = { min = 0.1, max = 1.0, }
else
	options.spellEffectLevel = { min = 0, max = 9, inst = 5, }
end

local next = next
local SetCVar = SetCVar
local IsInInstance = IsInInstance
local floor = math.floor
local log10 = math.log10
local active = true

for cvar, option in pairs(options) do
	local min = tonumber(GetCVarMin(cvar)) or option.min
	local max = tonumber(GetCVarMax(cvar)) or option.max
	local current = assert(tonumber(GetCVar(cvar)))
	option.min = min
	option.max = max
	option.current = current
	option.base = current

	if option.inst then
		assert(option.inst >= min and option.inst <= max)
	else
		option.inst = min
	end

	option.step = (max - min) / 100

	local _, _, a, c = GetCVarInfo(cvar)
	if a or c then
		print(string.format("perf: cvar %q is saved server-side. Updating it may increase network stress.", cvar))
	end
	-- print("perf", cvar, option.min, option.max, option.current)
end

local f = CreateFrame("Frame")

local UpdateTooltip
local prev_modifier = 0
local function Update(fps)
	local modifier
	dataobj.fps = fps
	if fps < TARGET_FPS then
		modifier = (fps - TARGET_FPS)^3 / 30
		if modifier < MIN_MOD_VALUE then
			modifier = MIN_MOD_VALUE
		end
	elseif fps > TARGET_FPS then
		modifier = (fps - TARGET_FPS)^3 / 60
		if modifier > MAX_MOD_VALUE then
			modifier = MAX_MOD_VALUE
		end
	else
		return
	end

	local diff_modifier = modifier - prev_modifier
	if diff_modifier < MAX_MOD_DECREASE then
		modifier = prev_modifier + MAX_MOD_DECREASE
	elseif diff_modifier > MAX_MOD_INCREASE then
		modifier = prev_modifier + MAX_MOD_INCREASE
	end
	prev_modifier = modifier
	dataobj.modifier = modifier

	-- print("perf modifier", fps, modifier)
	local minCheck = IsInInstance() and "inst" or "min"
	local qSum, qCount = 0.0, 0
	for cvar, option in next, options do
		local prev = option.current
		local current = prev + modifier * option.step
		local min, max = option[minCheck], option.max
		if current < min then
			current = min
		elseif current > max then
			current = max
		end
		if prev ~= current then
			SetCVar(cvar, current)
			option.current = current
		end
		qSum = qSum + (current - min) / (max - min)
		qCount = qCount + 1
	end
	dataobj.value = math.floor(0.5 + 100.0 * qSum / qCount)
	
	UpdateTooltip()
end

local time, count = TIME_BETWEEN_UPDATES, 0
f:SetScript("OnUpdate", function (_, elapsed)
	local t, c = time - elapsed, count + 1
	if t < 0 then
		time, count = TIME_BETWEEN_UPDATES, 0
		Update(c / (TIME_BETWEEN_UPDATES - t))
	else
		time, count = t, c
	end
end)
f:SetScript("OnEvent", function (self, event)
	if event == 'PLAYER_LOGOUT' then
		self:Hide()
	elseif event == 'PLAYER_ENTERING_WORLD' then
		if active then
			self:Show()
			time = 15
		end
	elseif event == 'PLAYER_LEAVING_WORLD' then
		self:Hide()
	end
end)
f:SetScript('OnShow', function(self)
	for cvar, option in pairs(options) do
		option.base = tonumber(GetCVar(cvar))
		option.current = option.base
	end
end)
f:SetScript('OnHide', function(self)
	for cvar, option in pairs(options) do
		SetCVar(cvar, option.base)
	end
	dataobj.value = "-"
end)
f:RegisterEvent("PLAYER_LOGOUT")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("PLAYER_LEAVING_WORLD")
f:Hide()

VideoOptionsEffectsPanel:HookScript('OnShow', function() f:Hide() end)
VideoOptionsEffectsPanel:HookScript('OnHide', function() if active then f:Show() end end)

local tooltip
function UpdateTooltip()
	if not tooltip then return end
	tooltip:ClearLines()
	tooltip:AddLine('DynPerf')
	tooltip:AddDoubleLine('Target FPS', TARGET_FPS)
	tooltip:AddDoubleLine('Current FPS', ("%.1f"):format(dataobj.fps))
	tooltip:AddDoubleLine('Current Modifier', ("%.1f"):format(dataobj.modifier))
	for name, option in pairs(options) do
		local step = 10^(floor(log10(option.max))-3)
		tooltip:AddDoubleLine(name, floor(option.current/step)*step)
	end
	tooltip:Show()
end

function dataobj.OnEnter(anchor)
	if not tooltip then
		tooltip = GameTooltip
		tooltip:SetOwner(anchor, 'ANCHOR_NONE')
		local _, anchorY = anchor:GetCenter()
		local _, screenY = UIParent:GetCenter()
		if anchorY >= screenY then
			tooltip:SetPoint('TOP', anchor, 'BOTTOM', 0, -1)
		else
			tooltip:SetPoint('BOTTOM', anchor, 'TOP', 0, 1)
		end
		UpdateTooltip()
	end
end

function dataobj.OnLeave(anchor)
	if tooltip then
		tooltip:Hide()
		tooltip = nil
	end
end

function dataobj.OnClick()
	if active then
		active = false
		f:Hide()
	else
		active = true
		f:Show()
	end
end
